﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using log4net;

namespace Kexp.ShoutcastRunner
{
	public class CrossdomainServer
	{
		private static readonly ILog Log = LogManager.GetLogger(typeof(CrossdomainServer));
		private const string Filename = "crossdomain.xml";
		private const int DefaultPort = 80;
		private const int DefaultShoutcastPort = 8000;
		private const int BufferSize = 4096;
		private const int FileCacheSize = 1024 * 8; // 8K worth of crossdomain cache
		private HttpListener _listener;
		private FileSystemWatcher _watcher;
        private readonly string _exeDirectory = Settings.ShoutcastExecutableDirectory;
		private string _error = string.Empty;
		private string _fileCache = string.Empty;
		private string _runnerLog;
		
		public void Start()
		{
			InitFileSystemWatcher();

			_runnerLog = GetRunnerLogFilename();

			_listener = new HttpListener();
			_listener.IgnoreWriteExceptions = true;
			_listener.Prefixes.Add(string.Format("http://+:{0}/", DefaultPort));
			_listener.Start();
			_listener.BeginGetContext(ProcessRequest, _listener);

			Log.Info("Crossdomain listener started.");
		}

		public void Stop()
		{
			if (_listener != null && 
				_listener.IsListening)
				_listener.Stop();

			if (_watcher != null)
			{
				_watcher.EnableRaisingEvents = false;
				_watcher.Dispose();
			}

			Log.Info("Crossdomain listener stopped.");
		}

		public void ProcessRequest(IAsyncResult result)
		{
			try
			{
				var listener = (HttpListener) result.AsyncState;
				if (!listener.IsListening)
				{
					// probably means this program is shutting down
					return;
				}

				var context = listener.EndGetContext(result);
				var requestFile = context.Request.Url.Segments.Last().Trim().ToLower();

				switch (requestFile)
				{
					case Filename:
						ReturnCrossdomainXml(context);
						break;
					case "runnerlog":
						ReturnRunnerLog(context);
						break;
					default:
						ReturnGenericMessage(context);
						break;
				}
			}
			catch (ProtocolViolationException pve)
			{
				// I could finally reproduce this if I make a HEAD request of the service. Got the idea from here:
				// http://stackoverflow.com/questions/6227534/getting-a-protocolviolationexception-with-google-chrome-and-httplistener
				// Seems to be fixed now, if it happens again there's another cause.
				//
				// Previous attempts to fix included:
		 		// http://stackoverflow.com/questions/1656717/c-sharp-httpwebrequest-posting-failing

				Log.Error(pve);
			}
			catch(Exception e)
			{
				Log.Error(e);
			}
			finally
			{
				_listener.BeginGetContext(ProcessRequest, _listener);
			}
		}

		private string GetRunnerLogFilename()
		{
			// todo: replace this with something that reads the log4net config
			return "shoutcastRunner-log.txt";
		}

		private void InitFileSystemWatcher()
		{
			ReadFileIntoCache();

			_watcher = new FileSystemWatcher
			{
				Path = _exeDirectory,
				NotifyFilter = NotifyFilters.LastWrite,
				Filter = Filename,
				IncludeSubdirectories = false,
				InternalBufferSize = 4096
			};

			_watcher.Changed += OnCrossdomainXmlChanged;
			_watcher.EnableRaisingEvents = true;
		}

		private void OnCrossdomainXmlChanged(object sender, FileSystemEventArgs e)
		{
			ReadFileIntoCache();
		}

		private void ReadFileIntoCache()
		{
			if (!_exeDirectory.IsNullOrEmptyOrWhiteSpace())
			{
				var crossdomainInfo = new FileInfo(Path.Combine(_exeDirectory, Filename));
				if (crossdomainInfo.Exists)
				{
					if (crossdomainInfo.Length < FileCacheSize)
					{
						using (var fileStream = new FileStream(crossdomainInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
						{
							using (var streamReader = new StreamReader(fileStream))
							{
								lock (_fileCache)
									_fileCache = streamReader.ReadToEnd();

								if (!string.IsNullOrEmpty(_error))
									lock (_error)
										_error = string.Empty;
							}
						}
					}
					else
					{
						// I thought about this, why would we need a crossdomain.xml over 8K, if even that?
						// If you do want to serve big files you can do it with ReturnStream()
						lock (_error)
							_error = "File is a little big.";
					}
				}
				else
				{
					lock (_error)
						_error = "No crossdomain.xml to be found.";
				}
			}
			else
			{
				lock (_error)
					_error = "This doesn't seem possible.";
			}
		}

		private void ReturnCrossdomainXml(HttpListenerContext context)
		{
			if (!_fileCache.IsNullOrEmptyOrWhiteSpace())
			{
				ReturnString(context, _fileCache, "text/xml", false);
			}
			else if (!_error.IsNullOrEmptyOrWhiteSpace())
			{
				ReturnString(context, _error, "text/plain");
			}
			else
			{
				ReturnString(context, "No file cache and no error, this is very strange.", "text/plain");
			}
		}

		private void ReturnGenericMessage(HttpListenerContext context)
		{
			var outputString = string.Format("<a href=\"http://{0}:{1}/listen.pls\">listen</a>", context.Request.Url.Host, DefaultShoutcastPort);
			ReturnString(context, outputString, "text/html");
		}

		private void ReturnRunnerLog(HttpListenerContext context)
		{
			if (!_exeDirectory.IsNullOrEmptyOrWhiteSpace())
			{
				var runnerLogInfo = new FileInfo(Path.Combine(_exeDirectory, _runnerLog));
				if (runnerLogInfo.Exists)
				{
					using (var fileStream = new FileStream(runnerLogInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
					{
						ReturnStream(context, fileStream, "text/plain");

						if (!string.IsNullOrEmpty(_error))
							lock (_error)
								_error = string.Empty;
					}
				}
				else
				{
					lock (_error)
						_error = "Log not found.";
				}
			}
			else
			{
				lock (_error)
					_error = "This doesn't seem possible.";
			}
		}

		private static void ReturnStream(HttpListenerContext context, FileStream fileStream, string outputType, bool allowCache = true)
		{
			var response = context.Response;
			response.KeepAlive = false;

			if (!allowCache)
				response.Headers.Add(HttpResponseHeader.CacheControl, "no-cache");

			response.ContentType = outputType;

			if (context.Request.HttpMethod.ToLower() != "head")
			{
				var buffer = new byte[BufferSize];
				int bytesRead = fileStream.Read(buffer, 0, BufferSize);
				while (bytesRead > 0)
				{
					response.OutputStream.Write(buffer, 0, bytesRead);
					bytesRead = fileStream.Read(buffer, 0, BufferSize);
				}
			}

			response.OutputStream.Flush();
			response.OutputStream.Close();
			response.Close();
		}

		private static void ReturnString(HttpListenerContext context, string outputString, string outputType, bool allowCache = true)
		{
			var response = context.Response;
			response.KeepAlive = false;
			Encoding enc = new UTF8Encoding(false);

			if (!allowCache)
				response.Headers.Add(HttpResponseHeader.CacheControl, "no-cache");

			response.ContentType = outputType;
			byte[] output = enc.GetBytes(outputString);
			response.ContentLength64 = output.Length;

			if (context.Request.HttpMethod.ToLower() != "head")
				response.OutputStream.Write(output, 0, output.Length);

			response.OutputStream.Flush();
			response.OutputStream.Close();
			response.Close();
		}
	}
}
